require "psync/node"

local setmetatable = _G.setmetatable
local print = _G.print
local assert = _G.assert
local require = _G.require
local pairs = _G.pairs
local error = _G.error

module "psync"

local proto = require "proto"

function create_sync_node(state)
	local node = create_node()
	
	node._type_reg = {}
	node._type_to_id = {}
	node._next_type_id = 0
	node._frames = {}
	node._cur_frame = 0 -- The frame that is being received (on non-auth) or built (on auth)

	state = state or {}
	state._base_frame = 0 -- The frame we are currently simulating as the base state
	node.state = state

	local auth = false
	local started = false
	local synced = false -- Initially, clients 
	local frame_finish_ac -- Single instance of frame_finish_proto
	local welcome_proto
	local replace_state_proto

	function node:objtype(desc)
		assert(not started)

		assert(desc.archive) -- Need archive function

		local type_id = self._next_type_id
		self._type_reg[type_id] = desc
		self._type_to_id[desc] = type_id
		self._next_type_id = type_id + 1
	end


	function node:action(desc)
		-- Check that all required functions are there
		assert(desc.apply)

		-- Register as a type
		self:objtype(desc)
	end

	function node:is_auth()
		return auth
	end

	function node:write_object(ch, obj)
		local type_id = self._type_to_id[proto.parent(obj)]
		assert(type_id)

		print("Sending object of type " .. type_id)

		ch.int(type_id) -- Type tag
		obj:archive(ch)
	end

	function node:read_object(ch)
		local type_id = ch.int()
		print("Received object of type " .. type_id)
		local parent_proto = self._type_reg[type_id]

		if parent_proto then
			local obj = parent_proto:create()
			obj:archive(ch)
			return obj
		end
	end

	-- Send an action to a specific link. This does not reject clients
	-- that are marked as not in-sync.
	-- The passed action object is only used during this call.
	local function send_action(link, ac)
		local ch = node:out_stream(link)
		node:write_object(ch, ac)
		ch.putend()
	end

	-- Send an action to all clients that are in-sync.
	-- The passed action object is only used during this call.
	local function broadcast_action(ac)
		for id,client in pairs(node._clients) do
			if client.synced then
				local ch = node:out_stream(id)
				node:write_object(ch, ac)
				ch.putend()
			end
		end
	end

	-- TODO: Instead of having immediate, perhaps store the
	-- scheduler (schedule_action by default) in
	-- the action base.
	
	local function schedule_action(ac, frame)
		-- ac.immediate means the action should be
		-- applied immediately. In effect, the action
		-- is supposed to schedule itself.
		if ac.immediate or frame == node.state._base_frame then
			ac:apply(node)
			return
		end

		local frames = node._frames
		local tab = frames[frame]
		if not tab then
			tab = {n = 0}
			frames[frame] = tab
		end

		local n = tab.n + 1
		tab[n] = ac
		tab.n = n
	end
	
	function node:message(bs)

		if bs.getfirst() == 0 then
			return
		end

		repeat
			local ac = self:read_object(bs)

			-- TODO: Actions may not be scheduled to
			-- be applied as soon as possible, but in
			-- a certain frame.

			-- TODO: If ac is nil, we should abort the link
			if not ac then
				error("Invalid main object read")
			end

			if auth then
				local link = bs.link
				ac.src_link = link

				print("Received request from client")

				if self._clients[link].synced then

					-- Broadcast is done before, because if
					-- the action causes a state sending
					-- the convention is to bring the state
					-- up to date first and then send.
					-- Broadcasting this message to the sender
					-- afterwards would mess with their state.
					broadcast_action(ac)

					schedule_action(ac, self._cur_frame)
				else
					print("Ignoring request from unsynced client")
				end
			else
				-- Received action from auth
				schedule_action(ac, self._cur_frame)
			end

		until bs.getnext() == 0
	end


	function node:request(ac)
		assert(started)

		if auth then
			schedule_action(ac, self._cur_frame)

			broastcast(ac)
		else
			-- Send to server
			local ch = self._server_out
			assert(ch)
			self:write_object(ch, ac)
			ch.putend()
		end
	end

	local parent_host = node.host
	function node:host(port)
		assert(not started)
		parent_host(self, port)
		self._clients = {}
		auth = true
		started = true
		print("Hosting on port " .. port)
	end

	local parent_connect = node.connect
	function node:connect(addr, port)
		assert(not started)
		self._server_link = parent_connect(self, addr, port)
		auth = false
		started = true
		print("Connecting to " .. addr .. ", port " .. port)
		return self._server_link
	end

	function node:event(ev, data)
		if ev == "link connect" then
			if auth then
				-- Client connected
				-- Initially, the client is not synced.
				-- It will only sync on request.
				self._clients[data] = {synced = false, link = data}

				local welcome_ac = welcome_proto:create()
				welcome_ac.psync_version = 0
				welcome_ac.own_link_id = data
				send_action(data, welcome_ac)

				-- TODO: This should perhaps not normally be done here
				self:bring_into_sync(data)
			else
				-- Server connected
				print("Server connected")
			end
		elseif ev == "link created" then
			if not auth then
				print("Server link created")

				self._server_out = self:out_stream(self._server_link)
				assert(self._server_out)
			end
		elseif ev == "link disconnect" then
			if auth then
				self._clients[data] = nil
			end
		end
	end

	-- Apply a list of actions to a state.
	-- actions is assumed to be an array with the actions and a member 'n' containing the count,
	-- or nil in which case this function does nothing.
	local function apply_actions(state, actions)
		if not actions then return end

		-- TODO: In order to apply an action to a specific state
		-- we must pass it to apply(). Some actions will always
		-- apply to the node though, so we cannot remove that parameter.

		-- TODO: In marf we cannot predict actions that will alter
		-- the node. We need a way to mark such actions and perhaps
		-- pass nil as the node parameter for all others in order to
		-- detect bugs.
		for i=1,actions.n do
			local action = actions[i]
			action:apply(node)
		end
	end

	local function simulate_frame()
		-- We presume that the action scheduler has applied
		-- actions for the base frame. TODO: This might not
		-- be true in the future with marf!
		print("Advancing a frame")
		node:advance()
		local next_frame = node.state._base_frame + 1
		node.state._base_frame = next_frame
		apply_actions(node.state, node._frames[next_frame])
	end

	function node:advance()
		
	end

	function node:attempt_end_base_frame()
		if auth then
			self:request(frame_finish_ac)
			-- Simulate a frame, which will increment _base_frame
			simulate_frame()
		else
			if self.state._base_frame < self._cur_frame then
				-- There are frames available
				-- Simulate a frame, which will increment _base_frame
				simulate_frame()
			end
		end
	end

	function node:bring_into_sync(link)
		local client = self._clients[link]

		if client.synced then
			return -- Sync is already maintained
		end

		client.synced = true

		-- Send state
		local ac = replace_state_proto:create()
		ac.state = self.state
		send_action(link, ac)
	end

	function node:replace_state(new_state)
		-- TODO: What more to do here?
		print("Replacing state")
		self.frames = {} -- Discard any scheduled frames
		self._cur_frame = self.state._base_frame -- Received frame the same as simulated
		self.state = new_state
	end

	-- NOTE: This archive function is not for archiving the node,
	-- but for a contained state.
	function node:archive(bs, state)
		-- TODO: Archive the contained state
		state._base_frame = bs.int(state._base_frame)
	end

	-- Initialization
	
	local frame_finish_proto = proto.base {
		immediate = true,
		archive = function(self, bs)
			print("Read/write frame finish action")
			-- Empty
		end,
		apply = function(self, n)
			print("Applied frame finish action")
			n._cur_frame = n._cur_frame + 1
		end
	}

	-- Message to send when a client has connected
	welcome_proto = proto.base {
		immediate = true,
		archive = function(self, bs)
			print("Read/write welcome action")
			self.psync_version = bs.int(self.psync_version)
			self.own_link_id = bs.int(self.own_link_id)
		end,
		apply = function(self, n)
			print("Applied welcome action")
			n.own_link_id = self.own_link_id
			n.psync_version = self.psync_version
		end
		--[[
			int psync_version
			int own_link_id
		]]
	}

	replace_state_proto = proto.base {
		immediate = true,
		archive = function(self, bs)
			print("Read/write replace state action")
			if bs.inp then
				self.state = {}
			end
			bs.node:archive(bs, self.state)
		end,
		apply = function(self, n)
			n:replace_state(self.state)
		end
	}

	frame_finish_ac = frame_finish_proto:create()

	node:action(frame_finish_proto)
	node:action(welcome_proto)
	node:action(replace_state_proto)

	return node
end

function enable_player_list(node, player_proto)
	node:action(add_player_proto)

	node.state.players = {}
	-- TODO: Lists of players by link id
	
	local add_player_proto = proto.base {
		archive = function(self, bs)
			print("Read/write add player action")

			-- TODO: Fix obj_typed
			self.player = bs.obj_typed(self.player, player_proto)
		end,
		apply = function(self, n)
			print("Applied add player action")
			local state = n.state
			state.players[#n.players + 1] = self.player
			if n:is_auth() then
				-- Sync after state is up to date
				n:bring_into_sync(self.src_link)
			end

			-- TODO: Add player to player list for the link id
		end
		--[[
			player_proto player
				Player object to add
		]]
	}

	local prev_archive = node.archive
	function node.archive(bs, state)
		-- TODO: Archive player list
		return prev_archive(self, bs, state)
	end

	local prev_event = node.event
	function node:event(ev, data)
		if ev == "link disconnect" then
			if auth then
				-- TODO: Issue a remove_player action
				-- for players belonging to the client
				-- data. 
			end
		end
		return prev_event(self, ev, data)
	end

	function node:add_player(player)
		local a = add_player_proto:create()
		a.player = player
		self:request(a)
	end
end

function clone(obj, map)
	local existing = map[obj]
	if existing ~= nil then
		return existing
	end
	local t = type(obj)
	if t == "table" then
		local cloner = obj.clone
		if type(cloner) == "function" then
			local obj2 = cloner(obj2, map)
			map[obj] = obj2 -- NOTE: If the object can be cyclic, the clone function must do this itself before it clones any references to itself
			return obj2
		else
			local obj2 = {}
			map[obj] = obj2
			for k,v in pairs(obj) do
				obj2[clone(k, map)] = clone(v, map)
			end
			-- Metatable is not cloned
			return setmetatable(obj2, getmetatable(obj))
		end
	elseif t == "userdata" then
		local cloner = obj.clone
		if type(cloner) == "function" then
			local obj2 = cloner(obj2, map)
			map[obj] = obj2
			return obj2
		end
	else
		return obj
	end
end